package behaviours;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.Behaviour;
import jade.core.behaviours.FSMBehaviour;
import jade.core.behaviours.OneShotBehaviour;
import jade.domain.FIPANames;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.lang.acl.UnreadableException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import com.myprofile.profile.MuseumItem;
import com.myprofile.profile.ProfileType;

import model.Profiler;
import model.Proposal;
import model.TourGuide;
import model.Tour;

@SuppressWarnings("serial")
public class TourNegotiationBehaviour extends FSMBehaviour {

	// When the tour negotiation is over this var will be filled with what we
	// bought
	// In the case of a Tour guide this will be a profile
	// In the case of a Profiler this will be an array of tours
	private Object boughtObject;
	
	private boolean myProposalAccepted = false;

	// Identifies the agent as a profiler or tourguide
	private boolean profiler;

	// Receiver
	private AID receiver;
	
	// Utility HashMap
	private HashMap<String, Integer> utilities;

	// Proposal Array
	private ArrayList<Proposal> proposals;
	private int proposalIndexToSend = 0;

	// Last received proposal
	private Proposal lastReceivedProposal;
	
	// A reference to the creator, needed later for casting purposes
	private Agent creator;
	
	// Finished proposals array
	private ArrayList<Proposal> finishedProposals;

	// States
	private final String STATE_LISTEN_FOR_PROPOSAL = "LISTEN_FOR_PROPOSAL";
	private final String STATE_PROCESS_PROPOSAL = "PROCESS_PROPOSAL";
	private final String STATE_SEND_PROPOSAL = "SEND_PROPOSAL";
	private final String STATE_ACCEPT_PROPOSAL = "ACCEPT_PROPOSAL";
	private final String STATE_LISTEN_FOR_CONFIRMATION = "LISTEN_FOR_CONFIRMATION";
	private final String STATE_END_NEGOTIATION = "END_NEGOTIATION";

	// OnEnds
	private final int PROPOSAL_REJECTED = 10;
	private final int PROPOSAL_ACCEPTED = 11;
	private final int END_OF_PROPOSALS = 12;
	private final int ACCEPT_PROPOSAL_RECEIVED = 13;
	private final int ACCEPT_PROPOSAL_NOT_RECEIVED = 14;

	public TourNegotiationBehaviour(HashMap<String, Integer> utilities,
			ArrayList<Proposal> proposals, boolean initiator, AID receiver,
			Agent creator, ArrayList<Proposal> finishedProposals) {

		this.finishedProposals = finishedProposals;
		this.receiver = receiver;
		this.utilities = utilities;
		this.proposals = proposals;
		this.creator = creator;
		this.profiler = initiator;
		
		// Register states
		if (initiator) {
			registerFirstState(new SendProposalBehaviour(), STATE_SEND_PROPOSAL);
			registerState(new ListenForProposalBehaviour(),
					STATE_LISTEN_FOR_PROPOSAL);

		} else {
			registerFirstState(new ListenForProposalBehaviour(),
					STATE_LISTEN_FOR_PROPOSAL);
			registerState(new SendProposalBehaviour(), STATE_SEND_PROPOSAL);
		}

		// This state is for calculating utility and making a decision based on
		// it
		registerState(new ProcessProposalBehaviour(), STATE_PROCESS_PROPOSAL);

		// This state is for sending an accept proposal message
		registerState(new SendAcceptProposalBehaviour(), STATE_ACCEPT_PROPOSAL);
		
		// This state is for listening after confirmation that our accept proposal was received
		registerState(new ListenForConfirmationBehaviour(), STATE_LISTEN_FOR_CONFIRMATION);

		// When all content has been sent and received, this behaviour is
		// activated, deciding our next move
		registerLastState(new EndNegotiationBehaviour(), STATE_END_NEGOTIATION);

		// Register transitions
		registerTransition(STATE_PROCESS_PROPOSAL, STATE_SEND_PROPOSAL,
				PROPOSAL_REJECTED);
		registerTransition(STATE_PROCESS_PROPOSAL, STATE_ACCEPT_PROPOSAL,
				PROPOSAL_ACCEPTED);
		registerDefaultTransition(STATE_SEND_PROPOSAL,
				STATE_LISTEN_FOR_PROPOSAL);
		registerTransition(STATE_PROCESS_PROPOSAL, STATE_END_NEGOTIATION,
				END_OF_PROPOSALS);
		registerTransition(STATE_LISTEN_FOR_PROPOSAL, STATE_END_NEGOTIATION,
				ACCEPT_PROPOSAL_RECEIVED);
		registerTransition(STATE_LISTEN_FOR_PROPOSAL, STATE_PROCESS_PROPOSAL,
				ACCEPT_PROPOSAL_NOT_RECEIVED);

		registerDefaultTransition(STATE_ACCEPT_PROPOSAL, STATE_LISTEN_FOR_CONFIRMATION);
		registerDefaultTransition(STATE_LISTEN_FOR_CONFIRMATION, STATE_END_NEGOTIATION);
	}

	private class ListenForProposalBehaviour extends Behaviour {

		private boolean receivedProposal = false;
		private boolean receivedAcceptProposal = false;

		@Override
		public void action() {
			ACLMessage msg = myAgent.receive(MessageTemplate
					.MatchProtocol(FIPANames.InteractionProtocol.FIPA_PROPOSE));
			if (msg != null) {
				try {
					// Msg received was a proposal
					if (msg.getPerformative() == ACLMessage.PROPOSE) {
						lastReceivedProposal = (Proposal) msg
								.getContentObject();
						System.out.println(myAgent.getName()
								+ ": Received a proposal.");
						receiver = msg.getSender();
						receivedProposal = true;
					}
					// Msg received was an accept proposal
					else if (msg.getPerformative() == ACLMessage.ACCEPT_PROPOSAL) {
						myProposalAccepted = true;
						System.out.println(myAgent.getName()
								+ ": Received accept proposal.");
						receivedAcceptProposal = true;
						receivedProposal = true;

						// Fetch content and handle it appropriately
						boughtObject = msg.getContentObject();
						
						finishedProposals.add(proposals.get(proposalIndexToSend-1));
						
						// Who are we?
						// Content is sender-dependent
						if (profiler) {
							// Send reply
							System.out.println(myAgent.getName()
									+ ": Was identified as a profiler.");
							// We should be the ones that accepted somebody
							// elses proposal
							if (myProposalAccepted) {
								System.out
										.println(myAgent.getName()
												+ ": Somebody else accepted our proposal");

								// Should probably be a confirm
								myAgent.addBehaviour(new SendProfileBehaviour(
										ACLMessage.CONFIRM));

							}
						} else {
							// Send reply
							System.out.println(myAgent.getName()
									+ ": Was identified as a tour guide.");
							// We should be the ones that accepted somebody
							// elses proposal
							if (myProposalAccepted) {
								System.out
										.println(myAgent.getName()
												+ ": Somebody else accepted our proposal");

								// Should probably be a confirm
								myAgent.addBehaviour(new SendTourBehaviour(
										ACLMessage.CONFIRM));
							}
						}
					}
				} catch (UnreadableException e) {
					System.out
							.println("Error when reading content object from proposal");
					e.printStackTrace();
				}
			} else
				block();
		}

		@Override
		public boolean done() {
			if (receivedProposal) {
				receivedProposal = false;
				return true;
			} else {
				return false;
			}
		}

		@Override
		public int onEnd() {
			if (receivedAcceptProposal) {
				return ACCEPT_PROPOSAL_RECEIVED;
			}
			return ACCEPT_PROPOSAL_NOT_RECEIVED;
		}
	}

	private class ProcessProposalBehaviour extends OneShotBehaviour {

		private boolean acceptProposal;

		@Override
		public void action() {
			System.out.println(myAgent.getName() + ": Processing proposal.");
			int utility = utility(lastReceivedProposal);
			if (utility >= 0)
				acceptProposal = true;
			else
				acceptProposal = false;
		}

		@Override
		public int onEnd() {
			// If we have no more proposals to send and we couldnt accept the
			// last proposal received
			if (proposalIndexToSend >= proposals.size() && !acceptProposal)
				return END_OF_PROPOSALS;

			if (acceptProposal)
				return PROPOSAL_ACCEPTED;
			return PROPOSAL_REJECTED;
		}
	}

	private int utility(Proposal p) {
		int utility = 0;
		// If we are not a profiler, update utilities!
		if (!profiler)
			utilities.put("P3", p.getVisistedItems() * 2);
		for (String buy : lastReceivedProposal.getBuyItems())
			utility -= utilities.get(buy);
		for (String sell : lastReceivedProposal.getSellItems())
			utility += utilities.get(sell);
		System.out.println(myAgent.getName()
				+ ": My utility for this proposal: " + utility);
		return utility;
	}

	private class SendProposalBehaviour extends OneShotBehaviour {

		@Override
		public void action() {
			System.out.println(myAgent.getName() + ": Sending proposal to "
					+ receiver.getName());
			ACLMessage msg = new ACLMessage(ACLMessage.PROPOSE);
			msg.setProtocol(FIPANames.InteractionProtocol.FIPA_PROPOSE);
			try {
				Proposal p = selectProposal();
				if (p != null) {
					System.out.println(myAgent.getName() + ": Proposal to send: "
							+ p);
					msg.setContentObject(p);
				} else {
					System.out.println(myAgent.getName() + ": Found no proposal to send!");
				}
			} catch (IOException e) {
				System.out
						.println("Error when setting content object in proposal message");
				e.printStackTrace();
			}
			msg.addReceiver(receiver);
			myAgent.send(msg);
		}

		/*
		 *	What we need to consider when deciding to skip a proposal or not:
		 *	* Is the item already sold?
		 *	* Is the item already bought?
		 *	* Don't sell P3 if P3 = 0
		 */
		private Proposal selectProposal() {

			// Construct sold and bought items
			ArrayList<String> tradedItems = new ArrayList<String>();
		
			for (Proposal p : finishedProposals) {
				for (String s : p.getSellItems())
					tradedItems.add(s);
				for (String s : p.getBuyItems())
					tradedItems.add(s);
			}
			
			// Loop until we find a proposal we can send
			while (true) {
				
				// Construct items to sell and buy
				ArrayList<String> itemsToTrade = new ArrayList<String>();
				for (String s : proposals.get(proposalIndexToSend).getSellItems())
					itemsToTrade.add(s);
				for (String s : proposals.get(proposalIndexToSend).getBuyItems())
					itemsToTrade.add(s);
				
				boolean proposalIsValid = true;
				
				// Have we already sold or bought this item?
				for (String item : itemsToTrade)
					for (String tradedItem : tradedItems)
						if (tradedItem.equalsIgnoreCase(item)) {
							proposalIsValid = false;
						}
							
				// Is P3 = 0?
				if (utilities.get("P3") == 0) {
					// Are we trying to sell or buy P3
					for (String item : itemsToTrade)
						if (item.equalsIgnoreCase("P3")) {
							proposalIsValid = false;
						}
				}
							
				if (proposalIsValid) {
					return proposals.get(proposalIndexToSend++);
				} else {
					if (proposalIndexToSend >= proposals.size() - 1)
						return null;
					proposalIndexToSend++;
				}
			}
		}
	}

	private class SendAcceptProposalBehaviour extends OneShotBehaviour {

		@Override
		public void action() {
			System.out.println(myAgent.getName()
					+ ": Sending accept proposal message to "
					+ receiver.getName());

			// If we got here we are the ones to send the accept proposal
			// message
			// The accept proposal message should contain the content we are
			// "paying"
			
			// Add this proposal to the list of finished proposals
			finishedProposals.add(lastReceivedProposal);
			
			// Who are we?
			// Content is sender-dependent
			if (profiler) {
				System.out.println(myAgent.getName()
						+ ": Was identified as a profiler.");
				// We should be the ones that accepted somebody elses proposal
				if (!myProposalAccepted) {
					System.out.println(myAgent.getName()
							+ ": We accepted somebody elses proposal");
					myAgent.addBehaviour(new SendProfileBehaviour(
							ACLMessage.ACCEPT_PROPOSAL));
				}
			} else {
				System.out.println(myAgent.getName()
						+ ": Was identified as a tour guide.");
				// We should be the ones that accepted somebody elses proposal
				if (!myProposalAccepted) {
					System.out.println(myAgent.getName()
							+ ": We accepted somebody elses proposal");
					myAgent.addBehaviour(new SendTourBehaviour(
							ACLMessage.ACCEPT_PROPOSAL));
				}
			}
		}
	}
	
	private class ListenForConfirmationBehaviour extends Behaviour {
		
		private boolean confirmationReceived = false;
		
		@Override
		public void action() {
			ACLMessage msg = myAgent.receive(MessageTemplate
					.MatchProtocol(FIPANames.InteractionProtocol.FIPA_PROPOSE));
			if (msg != null) {
					if(msg.getPerformative() == ACLMessage.CONFIRM) {
						System.out.println(myAgent.getName() + ": Received a confirmation of our accept proposal message.");
						
						// Fetch content and handle it appropriately
						try {
							boughtObject = msg.getContentObject();
							confirmationReceived = true;
						} 
						// Shouldnt happen
						catch (UnreadableException e) {
							e.printStackTrace();
						}
					}
			}
		}
		
		@Override
		public boolean done() {
			if (confirmationReceived) {
				confirmationReceived = false;
				return true;
			} else {
				return false;
			}
		}
		
		@Override
		public int onEnd() {
			return 0;
		}
	}

	private class SendProfileBehaviour extends OneShotBehaviour {

		private int performative;

		public SendProfileBehaviour(int performative) {
			super();
			this.performative = performative;
		}

		@Override
		public void action() {

			ACLMessage msg = new ACLMessage(performative);
			msg.setProtocol(FIPANames.InteractionProtocol.FIPA_PROPOSE);

			Profiler profiler = (Profiler) creator;
			ProfileType profile = profiler.getProfile();

			ProfileType sendProfile = new ProfileType();

			for (String s : proposals.get(proposalIndexToSend - 1)
					.getSellItems()) {
				if (s.contains("P1")) {
					sendProfile.setAge(profile.getAge());
					sendProfile.setEducation(profile.getEducation());
					sendProfile.setMotivationOfVisit(profile
							.getMotivationOfVisit());
					sendProfile.setPrivacy(profile.getPrivacy());
				}
				if (s.contains("P2")) {
					sendProfile.setInterests(profile.getInterests());
				}
				if (s.contains("P3")) {
					sendProfile.setVisitedItems(profile.getVisitedItems());
				}
			}

			try {
				msg.setContentObject(sendProfile);
			} catch (IOException e) {
				// Shouldnt happen
				e.printStackTrace();
			}
			msg.addReceiver(receiver);
			myAgent.send(msg);
		}
	}

	private class SendTourBehaviour extends OneShotBehaviour {

		private int performative;

		public SendTourBehaviour(int performative) {
			this.performative = performative;
		}

		@Override
		public void action() {
			// Send tour
			System.out.println(myAgent.getName() + ": Sending tour...");

			ACLMessage msg = new ACLMessage(performative);
			msg.setProtocol(FIPANames.InteractionProtocol.FIPA_PROPOSE);

			TourGuide tg = (TourGuide) creator;

			// An arraylist of arraylists (basically a list of tours)
			ArrayList<Tour> tours = new ArrayList<Tour>();
			
			Proposal p = null;
			String[] toursRequested = null;
			
			if(myProposalAccepted) {
				p = proposals.get(proposalIndexToSend - 1);
				toursRequested = p.getSellItems();
			}
			
			else {
				p = lastReceivedProposal;
				toursRequested = p.getBuyItems();
			}

			// For every requested tour in the proposal
			for (String s : toursRequested) {
				
				// We need to check if the profiler was greedy or normal
				// since we are going to send different tours
				if(model.Debug.greedy)				
					// For now, we choose normal
					tours.add(tg.getTour(s, "greedy"));
				else
					tours.add(tg.getTour(s, "normal"));
				
			}

			try {
				msg.setContentObject(tours);
			} catch (IOException e) {
				// Shouldnt happen
				e.printStackTrace();
			}
			msg.addReceiver(receiver);
			myAgent.send(msg);
		}
	}

	private class EndNegotiationBehaviour extends OneShotBehaviour {

		@Override
		public void action() {
			// TODO This will not work if we switch who the "accepter" will be

			if (profiler) {
				if(boughtObject != null) {
					
					ArrayList<Tour> tours = (ArrayList<Tour>) boughtObject;
					
					System.out.println(myAgent.getName() + ": Has bought tours!");
					System.out.println(myAgent.getName() + ": Number of tours we bought: " + tours.size());
					
					Profiler prof = (Profiler) creator;
					prof.storeTour(tours);
				}
				System.out.println(myAgent.getName()
						+ ": Negotiation ended. Moving to museum.");
				
				// Make a call to move to the museum
				myAgent.addBehaviour(new MoveToMuseumBehaviour());
			} else {
				
				if(boughtObject != null) {
					
					ProfileType boughtProfile = (ProfileType) boughtObject;
					
					for (String s : lastReceivedProposal
							.getSellItems()) {
						if(s.equals("P1")) {
							System.out.println(myAgent.getName() + ": Bought P1. Profile data as follows... ");
							// Age
							System.out.println(myAgent.getName() + ": Profile age = " + boughtProfile.getAge());
							// Education
							System.out.println(myAgent.getName() + ": Profile education = " + boughtProfile.getEducation());
							// Motivation of visit
							System.out.println(myAgent.getName() + ": Profile motivation of visit = " + boughtProfile.getMotivationOfVisit());
							// Privacy
							System.out.println(myAgent.getName() + ": Profile privacy = " + boughtProfile.getPrivacy());
						} else if(s.equals("P2")) {
							System.out.println(myAgent.getName() + ": Bought P2. Profile data as follows... ");
							// Interests
							List<String> interests = boughtProfile.getInterests().getInterest();
							for(String interest : interests) {
								System.out.println(myAgent.getName() + ": Profile interest: " + interest);
							}
						} else if(s.equals("P3")) {
							System.out.println(myAgent.getName() + ": Bought P3. Profile data as follows...");
							List<MuseumItem> visitedItems = boughtProfile.getVisitedItems().getVisitedItem();
							for(MuseumItem item : visitedItems) {
								System.out.println(myAgent.getName() + ": Visited item: " + item.getName());
							}
						}
					}
				}
 				
				System.out.println(myAgent.getName()
						+ ": Will resume listening for profiler requests");
				
				// TODO "Reset" the tour guide"
				myAgent.addBehaviour(new TourNegotiationBehaviour(utilities, proposals, false, null, myAgent, finishedProposals));
			}
		}
	}
}